<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace Think;

/**
 * ThinkPHP路由解析类
 */
class Route
{

  // 路由检测
  public static function check()
  {
    $depr   =   C('URL_PATHINFO_DEPR');
    $regx   =   preg_replace('/\.' . __EXT__ . '$/i', '', trim($_SERVER['PATH_INFO'], $depr));
    // 分隔符替换 确保路由定义使用统一的分隔符
    if ('/' != $depr) {
      $regx = str_replace($depr, '/', $regx);
    }
    // URL映射定义（静态路由）
    $maps   =   C('URL_MAP_RULES');
    if (isset($maps[$regx])) {
      $var    =   self::parseUrl($maps[$regx]);
      $_GET   =   array_merge($var, $_GET);
      return true;
    }
    // 动态路由处理
    $routes =   C('URL_ROUTE_RULES');
    if (!empty($routes)) {
      foreach ($routes as $rule => $route) {
        if (is_numeric($rule)) {
          // 支持 array('rule','adddress',...) 定义路由
          $rule   =   array_shift($route);
        }
        if (is_array($route) && isset($route[2])) {
          // 路由参数
          $options    =   $route[2];
          if (isset($options['ext']) && __EXT__ != $options['ext']) {
            // URL后缀检测
            continue;
          }
          if (isset($options['method']) && REQUEST_METHOD != strtoupper($options['method'])) {
            // 请求类型检测
            continue;
          }
          // 自定义检测
          if (!empty($options['callback']) && is_callable($options['callback'])) {
            if (false === call_user_func($options['callback'])) {
              continue;
            }
          }
        }
        if (0 === strpos($rule, '/') && preg_match($rule, $regx, $matches)) { // 正则路由
          if ($route instanceof \Closure) {
            // 执行闭包
            $result = self::invokeRegx($route, $matches);
            // 如果返回布尔值 则继续执行
            return is_bool($result) ? $result : exit;
          } else {
            return self::parseRegex($matches, $route, $regx);
          }
        } else { // 规则路由
          $len1   =   substr_count($regx, '/');
          $len2   =   substr_count($rule, '/');
          if ($len1 >= $len2 || strpos($rule, '[')) {
            if ('$' == substr($rule, -1, 1)) { // 完整匹配
              if ($len1 != $len2) {
                continue;
              } else {
                $rule =  substr($rule, 0, -1);
              }
            }
            $match  =  self::checkUrlMatch($regx, $rule);
            if (false !== $match) {
              if ($route instanceof \Closure) {
                // 执行闭包
                $result = self::invokeRule($route, $match);
                // 如果返回布尔值 则继续执行
                return is_bool($result) ? $result : exit;
              } else {
                return self::parseRule($rule, $route, $regx);
              }
            }
          }
        }
      }
    }
    return false;
  }

  // 检测URL和规则路由是否匹配
  private static function checkUrlMatch($regx, $rule)
  {
    $m1 = explode('/', $regx);
    $m2 = explode('/', $rule);
    $var = array();
    foreach ($m2 as $key => $val) {
      if (0 === strpos($val, '[:')) {
        $val    =   substr($val, 1, -1);
      }

      if (':' == substr($val, 0, 1)) { // 动态变量
        if ($pos = strpos($val, '|')) {
          // 使用函数过滤
          $val   =   substr($val, 1, $pos - 1);
        }
        if (strpos($val, '\\')) {
          $type = substr($val, -1);
          if ('d' == $type) {
            if (isset($m1[$key]) && !is_numeric($m1[$key]))
              return false;
          }
          $name = substr($val, 1, -2);
        } elseif ($pos = strpos($val, '^')) {
          $array   =  explode('-', substr(strstr($val, '^'), 1));
          if (in_array($m1[$key], $array)) {
            return false;
          }
          $name = substr($val, 1, $pos - 1);
        } else {
          $name = substr($val, 1);
        }
        $var[$name] = isset($m1[$key]) ? $m1[$key] : '';
      } elseif (0 !== strcasecmp($val, $m1[$key])) {
        return false;
      }
    }
    // 成功匹配后返回URL中的动态变量数组
    return $var;
  }

  // 解析规范的路由地址
  // 地址格式 [控制器/操作?]参数1=值1&参数2=值2...
  private static function parseUrl($url)
  {
    $var  =  array();
    if (false !== strpos($url, '?')) { // [控制器/操作?]参数1=值1&参数2=值2...
      $info   =  parse_url($url);
      $path   = explode('/', $info['path']);
      parse_str($info['query'], $var);
    } elseif (strpos($url, '/')) { // [控制器/操作]
      $path = explode('/', $url);
    } else { // 参数1=值1&参数2=值2...
      parse_str($url, $var);
    }
    if (isset($path)) {
      $var[C('VAR_ACTION')] = array_pop($path);
      if (!empty($path)) {
        $var[C('VAR_CONTROLLER')] = array_pop($path);
      }
      if (!empty($path)) {
        $var[C('VAR_MODULE')]  = array_pop($path);
      }
    }
    return $var;
  }

  // 解析规则路由
  // '路由规则'=>'[控制器/操作]?额外参数1=值1&额外参数2=值2...'
  // '路由规则'=>array('[控制器/操作]','额外参数1=值1&额外参数2=值2...')
  // '路由规则'=>'外部地址'
  // '路由规则'=>array('外部地址','重定向代码')
  // 路由规则中 :开头 表示动态变量
  // 外部地址中可以用动态变量 采用 :1 :2 的方式
  // 'news/:month/:day/:id'=>array('News/read?cate=1','status=1'),
  // 'new/:id'=>array('/new.php?id=:1',301), 重定向
  private static function parseRule($rule, $route, $regx)
  {
    // 获取路由地址规则
    $url   =  is_array($route) ? $route[0] : $route;
    // 获取URL地址中的参数
    $paths = explode('/', $regx);
    // 解析路由规则
    $matches  =  array();
    $rule =  explode('/', $rule);
    foreach ($rule as $item) {
      $fun    =   '';
      if (0 === strpos($item, '[:')) {
        $item   =   substr($item, 1, -1);
      }
      if (0 === strpos($item, ':')) { // 动态变量获取
        if ($pos = strpos($item, '|')) {
          // 支持函数过滤
          $fun  =  substr($item, $pos + 1);
          $item =  substr($item, 0, $pos);
        }
        if ($pos = strpos($item, '^')) {
          $var  =  substr($item, 1, $pos - 1);
        } elseif (strpos($item, '\\')) {
          $var  =  substr($item, 1, -2);
        } else {
          $var  =  substr($item, 1);
        }
        $matches[$var] = !empty($fun) ? $fun(array_shift($paths)) : array_shift($paths);
      } else { // 过滤URL中的静态变量
        array_shift($paths);
      }
    }

    if (0 === strpos($url, '/') || 0 === strpos($url, 'http')) { // 路由重定向跳转
      if (strpos($url, ':')) { // 传递动态参数
        $values = array_values($matches);
        $url = preg_replace_callback('/:(\d+)/', function ($match) use ($values) {
          return $values[$match[1] - 1];
        }, $url);
      }
      header("Location: $url", true, (is_array($route) && isset($route[1])) ? $route[1] : 301);
      exit;
    } else {
      // 解析路由地址
      $var  =  self::parseUrl($url);
      // 解析路由地址里面的动态参数
      $values  =  array_values($matches);
      foreach ($var as $key => $val) {
        if (0 === strpos($val, ':')) {
          $var[$key] =  $values[substr($val, 1) - 1];
        }
      }
      $var   =   array_merge($matches, $var);
      // 解析剩余的URL参数
      if (!empty($paths)) {
        preg_replace_callback('/(\w+)\/([^\/]+)/', function ($match) use (&$var) {
          $var[strtolower($match[1])] = strip_tags($match[2]);
        }, implode('/', $paths));
      }
      // 解析路由自动传入参数
      if (is_array($route) && isset($route[1])) {
        if (is_array($route[1])) {
          $params     =   $route[1];
        } else {
          parse_str($route[1], $params);
        }
        $var   =   array_merge($var, $params);
      }
      $_GET   =  array_merge($var, $_GET);
    }
    return true;
  }

  // 解析正则路由
  // '路由正则'=>'[控制器/操作]?参数1=值1&参数2=值2...'
  // '路由正则'=>array('[控制器/操作]?参数1=值1&参数2=值2...','额外参数1=值1&额外参数2=值2...')
  // '路由正则'=>'外部地址'
  // '路由正则'=>array('外部地址','重定向代码')
  // 参数值和外部地址中可以用动态变量 采用 :1 :2 的方式
  // '/new\/(\d+)\/(\d+)/'=>array('News/read?id=:1&page=:2&cate=1','status=1'),
  // '/new\/(\d+)/'=>array('/new.php?id=:1&page=:2&status=1','301'), 重定向
  private static function parseRegex($matches, $route, $regx)
  {
    // 获取路由地址规则
    $url   =  is_array($route) ? $route[0] : $route;
    $url   =  preg_replace_callback('/:(\d+)/', function ($match) use ($matches) {
      return $matches[$match[1]];
    }, $url);
    if (0 === strpos($url, '/') || 0 === strpos($url, 'http')) { // 路由重定向跳转
      header("Location: $url", true, (is_array($route) && isset($route[1])) ? $route[1] : 301);
      exit;
    } else {
      // 解析路由地址
      $var  =  self::parseUrl($url);
      // 处理函数
      foreach ($var as $key => $val) {
        if (strpos($val, '|')) {
          list($val, $fun) = explode('|', $val);
          $var[$key]    =   $fun($val);
        }
      }
      // 解析剩余的URL参数
      $regx =  substr_replace($regx, '', 0, strlen($matches[0]));
      if ($regx) {
        preg_replace_callback('/(\w+)\/([^\/]+)/', function ($match) use (&$var) {
          $var[strtolower($match[1])] = strip_tags($match[2]);
        }, $regx);
      }
      // 解析路由自动传入参数
      if (is_array($route) && isset($route[1])) {
        if (is_array($route[1])) {
          $params     =   $route[1];
        } else {
          parse_str($route[1], $params);
        }
        $var   =   array_merge($var, $params);
      }
      $_GET   =  array_merge($var, $_GET);
    }
    return true;
  }

  // 执行正则匹配下的闭包方法 支持参数调用
  static private function invokeRegx($closure, $var = array())
  {
    $reflect = new \ReflectionFunction($closure);
    $params  = $reflect->getParameters();
    $args    = array();
    array_shift($var);
    foreach ($params as $param) {
      if (!empty($var)) {
        $args[] = array_shift($var);
      } elseif ($param->isDefaultValueAvailable()) {
        $args[] = $param->getDefaultValue();
      }
    }
    return $reflect->invokeArgs($args);
  }

  // 执行规则匹配下的闭包方法 支持参数调用
  static private function invokeRule($closure, $var = array())
  {
    $reflect = new \ReflectionFunction($closure);
    $params  = $reflect->getParameters();
    $args    = array();
    foreach ($params as $param) {
      $name = $param->getName();
      if (isset($var[$name])) {
        $args[] = $var[$name];
      } elseif ($param->isDefaultValueAvailable()) {
        $args[] = $param->getDefaultValue();
      }
    }
    return $reflect->invokeArgs($args);
  }
}
